require('./buildAndKeepPools.js');

const oracledb = require('oracledb');
const Koa = require('koa');

const { registry } = require('./servicesMap.js');
const readLobAsync = require('./readLobAsync.js');

const statsMap = new Map();

const app = new Koa();
global.oracledb = oracledb;

// 如果是 content-type:text/* 则强制认为是 application/json
// 避免浏览器直接发 application/json 造成 Preflighted requests
// 需要在 koa-body 自动解析前设置完毕
app.use(async (ctx, next) => {
  const contentType = ctx.headers['content-type'];
  if (contentType && contentType.match(/^text\//)) {
    ctx.headers['content-type'] = 'application/json';
  }
  await next();
});

// 获取请求体，解析成 js 数据，目前看必须是第一个中间件
app.use(require('koa-body')({
  formLimit: 10 * 1000 * 1000, // 上传大小 10M 以内都允许，默认是 56kb
}));

// 任何情形，允许跨域访问
app.use(async (ctx, next) => {
  await next();
  ctx.set('Access-Control-Allow-Origin', '*');
});

app.use(async (ctx, next) => {
  const msg = ctx.request.body;
  console.log(msg, ctx.query);
  ctx.state.req = { ...msg, ...ctx.query };

  const m = registry[ctx.path];
  if (!m) {
    ctx.throw(404, `no service for the path ${ctx.path}`);
  }

  const poolName = m.pool || ctx.path.split('/')[1]; // 默认路径第一部分就是pool名称

  if (!statsMap[m.path]) {
    statsMap[m.path] = {
      enterCount: 0,
      leaveCount: 0,
    };
  }
  const stats = statsMap[m.path];

  const concurrency = m.concurrency || Math.ceil(oracledb.getPool(poolName).poolMin / 3);
  // console.log(`${m.path}在途执行量${JSON.stringify(stats)}阀值${concurrency}`);
  if (stats.enterCount - stats.leaveCount >= concurrency) {
    ctx.response.body = JSON.stringify({
      respCode: -1,
      respDesc: `${m.path}在途执行量${JSON.stringify(stats)}超过阀值${concurrency}，禁止执行`,
    }, null, 2);
    await next();
    return;
  }

  const sqltext = (() => {
    if (m.sqltext instanceof Function) {
      return m.sqltext(ctx.state.req);
    }
    return m.sqltext;
  })();

  const bindObj = {};
  // console.log(1, sqltext.match(/:\w{3,}/gm));
  const match = sqltext.match(/(?::)\w{3,}/gm);
  if (match) {
    // 无匹配 match=null，有返回匹配数组
    match.forEach((param) => {
      const para = param.substr(1);
      const bindCfg = m.bindObj && m.bindObj[para];
      if (bindCfg) {
        switch (bindCfg.dir) {
          case oracledb.BIND_OUT:
            bindObj[para] = { ...bindCfg };
            break;
          case oracledb.BIND_IN:
          case oracledb.BIND_INOUT:
            bindObj[para] = { ...bindCfg, val: ctx.state.req[para] };
            break;
          default:
        }
      } else if (para.match(/Out/)) {
        const bindType = para.replace(/^(\w+)Out(\w*)$/, '$2') || 'DEFAULT';
        bindObj[para] = {
          dir: oracledb.BIND_OUT,
          type: oracledb[bindType.toUpperCase()],
        };
      } else if (para.endsWith('Cur')) {
        bindObj[para] = {
          dir: oracledb.BIND_OUT,
          type: oracledb.CURSOR,
        };
      } else {
        const value = ctx.state.req[para];
        bindObj[para] = {
          dir: oracledb.BIND_IN,
          val: String(value),
          // type: oracledb[(typeof value).toUpperCase()], // 支持 NUMBER, STRING
        };
      }
    });
  }
  // console.log('execute', m.path, sqltext, bindObj);
  stats.enterCount += 1;
  let sqlresult;
  let connection;
  try {
    connection = await oracledb.getConnection(poolName);
    connection.module = 'ora-sql-lib';
    connection.action = ctx.path;
    // connection.clientId 回头写 staffId 或者 callId
    sqlresult = await connection.execute(sqltext, bindObj, m.options || {});

    // 查看有 outBinds.xxx.metaData 情况
    const outBinds = sqlresult.outBinds;
    for (let i = 0, keys = Object.keys(bindObj), len = keys.length; i < len; i += 1) {
      const n = keys[i];
      const bindConfig = bindObj[n];
      if (bindConfig.type === oracledb.CURSOR) {
        try {
          outBinds[n].rows = await outBinds[n].getRows(100);
          await outBinds[n].close();
        } catch (e) {
          // refCursor 没有 open
        }
      } else if (bindConfig.type === oracledb.CLOB) {
        outBinds[n] = await readLobAsync(outBinds[n]);
      }
      const outName = n.match(/^(.*)(Out|Cur)($|Number|Date|String|Clob|Blob|Buffer)/);
      if (outName) {
        const key = outName[1];
        const suffix = outName[2];
        if (suffix) {
          outBinds[key] = outBinds[n];
          delete outBinds[n];
        }
      }
    }

    if (sqlresult.rowsAffected || !sqlresult.rows) {
      await connection.commit();
    }
    // console.log(m.sqlresult);
    ctx.response.body = JSON.stringify({
      data: m.outConverter ? m.outConverter(sqlresult, ctx.state.req) : sqlresult,
      respCode: 0,
      respDesc: `${m.title} - 成功`,
    }, null, 2);
  } catch (e) {
    ctx.response.body = JSON.stringify({
      respCode: e.errorNum,
      respDesc: `${m.title} - ${e.message}`,
      meta: {
        sqltext,
        bindObj,
      },
    }, null, 2);
  }
  // When a connection is released, any ongoing transaction on the connection is rolled back.
  if (connection) await connection.close();
  stats.leaveCount += 1;
  await next();
});

app.listen(process.env.PORT || 3014, '0.0.0.0');
